#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import absolute_import
# http://docs.python.org/whatsnew/pep-328.html

from __future__ import print_function   # deletes the print statement
from __future__ import unicode_literals # unicode by default

from datetime import datetime, timedelta

class Kodeko(object):
    '''Base class for Pykesto serializer/deserializers.'''
    prefix = b'o:'
    fortypes = [object]
    
    def encode(self, o):
        return repr(o)
    
    def decode(self, byts):
        return eval(byts)


class KNumber(Kodeko):
    prefix = b'0'
    fortypes = [int, float]
    
    def encode(self, o):
        if isinstance(o, int):
            return repr(o)
        elif isinstance(o, float):
            return str(o)
    
    def decode(self, byts):
        return eval(byts)


class KBoolean(Kodeko):
    prefix = b'B:'
    fortypes = [bool]
    
    def encode(self, o):
        return b'1' if o else b'0'
    
    def decode(self, byts):
        return True if byts == b'1' else False


class KBytes(Kodeko):
    prefix = b'b:'
    fortypes = [bytes]
    
    def encode(self, o):
        return o
    
    def decode(self, byts):
        return byts


class KString(Kodeko):
    prefix = b's:'
    fortypes = [unicode]
    
    def __init__(self, encoding='utf-8'):
        self.encoding = encoding
    
    def encode(self, s):
        return s.encode(self.encoding)
    
    def decode(self, byts):
        return byts.decode(self.encoding)


class KDate(Kodeko):
    prefix = b'd:'
    fortypes = [datetime]
    
    def encode(self, o):
        return o.isoformat() # 2000-12-25T00:00:00
    
    def decode(self, byts):
        return datetime.strptime(byts, '%Y-%m-%dT%H:%M:%S')


class KTimeDelta(Kodeko):
    prefix = b'td:'
    fortypes = [timedelta]
    
    def encode(self, o):
        return b'|'.join(o.days, o.seconds, o.microseconds)
        # TODO: This representation is probably not sortable
    
    def decode(self, byts):
        days, seconds, microseconds = byts.strip(b'|')
        return timedelta(days, seconds, microseconds)



class KodekoException(Exception):
    pass



class Serdes(object):
    '''Aggregates kodekos and uses them automatically to serialize or
    deserialize values. Also responsible for serializing and deserializing
    column names.
    
    To add support for new primitive types you should subclass Kodeko for
    your type, then use the Serdes.add() method to add it.
    
    You can subclass Serdes to change how columns are serialized. Then pass
    an instance of the subclass to the TCTable constructor.
    '''
    def __init__(self, add_primitives=True, encoding='utf-8'):
        self.encoding = encoding
        self.kodekos  = dict()
        self.prefixes = dict()
        if add_primitives:
            self.add(KNumber())
            self.add(KBoolean())
            self.add(KBytes())
            self.add(KString(encoding=encoding))
            self.add(KDate())
            self.add(KTimeDelta())
            # TODO: object (link), list, dict, Decimal, time, complex etc.
    
    def add(self, kodeko):
        # First check if a kodeko for that type already exists
        for typ in kodeko.fortypes:
            if self.kodekos.get(typ):
                raise KodekoException('A kodeko for type "{0}" is already ' \
                    'registered.'.format(typ))
        # Then check if that prefix is already registered
        prefix = kodeko.prefix[:-1]
        if self.prefixes.get(prefix):
            raise KodekoException('A kodeko with prefix "{0}" is already ' \
                'registered.'.format(kodeko.prefix))
        # Register this kodeko
        self.prefixes[prefix] = kodeko
        for typ in kodeko.fortypes:
            self.kodekos[typ] = kodeko
    
    def serialize_val(self, val):
        kodeko = self.kodekos.get(type(val), None)
        if kodeko is None:
            raise KodekoException('No kodeko registered for type "{0}"' \
                .format(type(val)))
        return bytes(kodeko.prefix + kodeko.encode(val))
    
    def deserialize_val(self, val):
        if val.startswith(b'0'):
            kodeko = self.prefixes[b'']
            val = val[1:]
        else:
            prefix = val.split(b':', 1)
            kodeko = self.prefixes.get(prefix[0])
            if kodeko is None:
                raise KodekoException('No kodeko registered with prefix ' \
                    '"{0}"'.format(prefix))
            val = val[len(kodeko.prefix):]
        return kodeko.decode(val)
    
    def serialize_col(self, col):
        '''Takes a *col* parameter which should be a unicode object, and
        returns a bytes object that can be written to disk.
        '''
        return col.encode(self.encoding)
    
    def deserialize_col(self, col):
        '''Takes a *col* parameter which is a bytes object read from disk, and
        returns a unicode object to be used in Python.
        '''
        return col.decode(self.encoding)



KEY_STATE    = '_k_state' # key to a row that stores global table state
COL_COUNTER  = 'last_key' # The state contains this column
COL_ID       = '_k_id' # Persisted dicts store their ID at this key

from .tctable import TCTable


class TableState(object):
    '''Controls the table state, especially
    autoincrementing the last key when new_key() is called.
    
    To replace the autoincrement strategy (e.g. with a globally unique
    identifier strategy), subclass TableState.
    
    Also responsible for serialization and deserialization of IDs (row keys).
    '''
    def __init__(self, tctable):
        self.tctable = tctable
        state_row = tctable.raw_get(KEY_STATE)
        self.last_key = 0 if state_row is None else \
                        self.deserialize_key(state_row[COL_COUNTER])
    
    def persist(self):
        self.tctable[KEY_STATE] = {COL_COUNTER:self.last_key}
    
    def new_key(self, persist=True):
        '''Autoincrements last_key and returns it. Also persists the new
        last_key to disk if you leave the *persist* parameter as True
        (the default).
        '''
        self.last_key = self.last_key + 1
        if persist:  self.persist()
        return self.last_key
    
    def serialize_key(self, key):
        return bytes(key)
    
    def deserialize_key(self, key):
        return int(key)



class SchemalessTable(TCTable):
    '''An on-disk table, using a Tokyo Cabinet table database as its backend.
    It can store, retrieve and query various common Python data types
    in a schemaless way.
    
    The constructor takes these parameters:
    
    *path* is the path to the database file to be opened. Its extension
    is normally .tct.
    
    *mode* is either 'w' for write or 'r' for read.
    
    *serdes* (optional argument) is a Serdes instance, responsible for
    serializing and deserializing column names and values.
    
    *tablestate* (optional argument) is a class (TableState by default)
    responsible for creating and persisting the table keys (IDs).
    
    The object can be seen as a Python dictionary -- whose values are
    dictionaries representing columns and values. However, put() and query
    are more useful than the dictionary interface.
    '''
    def __init__(self, path, serdes=None, tablestate=TableState, **k):
        super(SchemalessTable, self).__init__(path, **k)
        self.tablestate = tablestate(self)
        self.serdes = Serdes() if serdes is None else serdes
    
    def _serialize_dict(self, dic):
        serialize_val = self.serdes.serialize_val
        serialize_col = self.serdes.serialize_col
        dic.pop(COL_ID, None) # The dict should never contain the ID
        d = {}
        for col, val in dic.items():
            d[serialize_col(col)] = serialize_val(val)
        return d
    
    def __setitem__(self, key, dic):
        '''Writes a table row.'''
        super(SchemalessTable, self).__setitem__ \
            (self.tablestate.serialize_key(key), self._serialize_dict(dic))
    
    def keep_or_put(self, key, dic):
        '''Takes as arguments a key (string) and a value (dict).
        If the key already exists in the table, nothing is done
        and the string 'keep' is returned.
        Otherwise, the value is stored at the key and 'put' is returned.
        '''
        return super(SchemalessTable, self).keep_or_put \
            (self.tablestate.serialize_key(key), self._serialize_dict(dic))
    
    def _deserialize_dict(self, dic, key):
        '''Helper method that deserializes the keys and values of a given dict.
        '''
        deserialize_val = self.serdes.deserialize_val
        deserialize_col = self.serdes.deserialize_col
        d = {}
        for col, val in dic.items():
            d[deserialize_col(col)] = deserialize_val(val)
        if key is not None:  d[COL_ID] = key
        return d
        
    def __getitem__(self, key):
        '''Returns a record (as a dict) or raises KeyError.'''
        dic = super(SchemalessTable, self).__getitem__ \
            (self.tablestate.serialize_key(key))
        return self._deserialize_dict(dic, key)
    
    def get(self, key, default=None):
        '''Returns a row (as a dict) or the value of the *default* argument.
        '''
        dic = super(SchemalessTable, self).get \
            (self.tablestate.serialize_key(key), default)
        return self._deserialize_dict(dic, key)
    
    def raw_get(self, key, default=None):
        '''Returns a row (as a dict) or the value of the *default* argument,
        without serializing the key or deserializing the values. In other words
        this lets you see exactly how things are stored in the .tct file.
        '''
        return super(SchemalessTable, self).get(str(key), default)
    
    def __delitem__(self, key):
        return super(SchemalessTable, self).__delitem__ \
            (self.tablestate.serialize_key(key))
    
    def size_of(self, key):
        '''Returns the size of the value stored at the passed *id*,
        or -1 if the id does not exist in the table database.
        
        I don't know why this would be useful... the returned number is
        a little higher than the amount of characters persisted.
        '''
        return super(SchemalessTable, self).size_of \
            (self.tablestate.serialize_key(key))
    
    @property
    def query(self):
        '''Returns a new query object for searching this table.'''
        return SchemalessQuery(super(SchemalessTable, self).query, self)
    
    def pop(self, k, d=None):
        '''D.pop(k[,d]) -> v, remove specified key and return the
        corresponding value. If key is not found, d is returned if given,
        otherwise KeyError is raised.
        '''
        adic = super(SchemalessTable, self).pop \
            (self.tablestate.serialize_key(k), d)
        if adic is d:
            return adic
        else:
            return self._deserialize_dict(adic, None)
    
    def put(self, adict):
        '''Saves the dictionary adict to the database.'''
        key = adict.get(COL_ID, None)
        if key is None:  key = self.tablestate.new_key()
        self[key] = adict
    
    def put_many(self, dicts):
        '''Saves the dictionaries *dicts* to the database.'''
        for d in dicts:  self.put(d)



class SchemalessQuery(object):
    '''Represents a query to a SchemalessTable. You should never instantiate
    this yourself. Instead, obtain an instance from the query property of
    your table:
    
        # Query for all objects whose type is NOT 'person'
        query = mytable.query.col('type').no.equals('person')
        all_IDs = query.keys() # a list
        print "Deleting ", all_IDs
        query.delete_all()
    '''
    def __init__(self, tcquery, table):
        self._tcquery = tcquery
        self._table = table
    
    def count(self):
        '''Executes the query and returns the number of matching elements.
        Only use this when you really care only for the number of rows.
        If you call count() to know the number of rows, then keys() to get them
        you are actually performing the query twice, so don't.
        '''
        return self._tcquery.count()
    
    def keys(self):
        '''Executes the query and returns a list of the matching IDs...
        which could be empty.
        '''
        deserialize_key = self._table.tablestate._deserialize_key
        return [deserialize_key(k) for k in self._tcquery.keys() \
                if k != KEY_STATE]
    
    def iterkeys(self):
        '''Executes the query and returns a generator of the matching IDs...
        which could be empty.
        '''
        deserialize_key = self._table.tablestate._deserialize_key
        return (deserialize_key(k) for k in self._tcquery.keys() \
                if k != KEY_STATE)
    
    def delete_all(self):
        '''Executes the query and removes all corresponding objects.'''
        self._tcquery.delete_all()
        return self
    
    def all(self):
        '''Executes the query and returns a list containing the matching rows.
        '''
        deserialize_dict = self._table._deserialize_dict
        get = self._table.get
        alist = []
        for key in self.iterkeys:
            alist.append(deserialize_dict(get(key), key))
        return alist
    
    def col(self, name):
        '''Sets the column to which will be applied the next condition
        called on this query.
        '''
        self._tcquery.col(self._table.serdes.serialize_col(name))
        return self
    
    def order_by(self, col, desc=False, number=False):
        '''Sets the order of the query results, with these arguments:
        *col*: the name of the column to order by
        *desc*: descending order. The default is False (ascending).
        *number*: use the number sorting algorithm. Default is False
        (use string sorting algorithm).
        '''
        self._tcquery.order_by(self._table.serdes.serialize_col(col),
                               desc=desc, number=number)
        return self
    
    def limit(self, max=-1, offset=0):
        '''*max* specifies the maximum number of records in the result.
        The default is None.
        
        *offset* specifies the number of skipped records in the result.
        The default is None.
        '''
        self._tcquery.limit(max=max, offset=offset)
        return self
    
    @property
    def no(self):
        '''Sets up for negation the next condition called on this query.
        Example:
        
            mytable.query.no.endswith('name', 'Palin')
        '''
        self._tcquery.no
        return self
    
    def equals(self, val, col=None):
        '''Adds a condition to the query: the value of column *col* must
        equal *val*. Returns the same query object.
        '''
        self._tcquery.equals(self._table.serdes.serialize_val(val), col = \
            None if col is None else self._table.serdes.serialize_col(col))
        return self
    
    def contains(self, val, col=None):
        '''Adds a condition to the query: the value of column *col* must
        contain *val*. Returns the same query object.
        '''
        self._tcquery.contains(self._table.serdes.serialize_val(val), col = \
            None if col is None else self._table.serdes.serialize_col(col))
        return self
    
    def startswith(self, val, col=None):
        '''Adds a condition to the query: the value of column *col* must
        begin with *val*. Returns the same query object.
        '''
        self._tcquery.startswith(self._table.serdes.serialize_val(val), col = \
            None if col is None else self._table.serdes.serialize_col(col))
        return self
    
    def endswith(self, val, col=None):
        '''Adds a condition to the query: the value of column *col* must
        end with *val*. Returns the same query object.
        '''
        self._tcquery.endswith(self._table.serdes.serialize_val(val), col = \
            None if col is None else self._table.serdes.serialize_col(col))
        return self
    
    def contains_all(self, val, col=None):
        '''Adds a condition to the query: the value of column *col* must
        include all tokens in *val*, which may be a list,
        or preferably a string. Returns the same query object.
        '''
        if isinstance(val, list):
            val = ' '.join(val)
        self._tcquery.contains_all(self._table.serdes.serialize_val(val), \
            col=None if col is None else self._table.serdes.serialize_col(col))
        return self
    
    def contains_one(self, val, col=None):
        '''Adds a condition to the query: the value of column *col* must
        include one of the words in *val*, which may be a list,
        or preferably a string.
        
        Returns the same query object.
        '''
        if isinstance(val, list):
            val = ' '.join(val)
        self._tcquery.contains_one(self._table.serdes.serialize_val(val), \
            col=None if col is None else self._table.serdes.serialize_col(col))
        return self
    
    def equals_one(self, val, col=None):
        '''Adds a condition to the query: the value of column *col* must
        equal one of the words in *val*, which may be a list,
        or preferably a string.
        
        Returns the same query object.
        '''
        if isinstance(val, list):
            val = ' '.join(val)
        self._tcquery.contains_one(self._table.serdes.serialize_val(val), \
            col=None if col is None else self._table.serdes.serialize_col(col))
        return self
    
    def matches_regex(self, val, col=None):
        '''Adds a condition to the query: the value of column *col* must
        match the regular expression pattern in *val*.
        Returns the same query object.
        '''
        self._tcquery.matches_regex(self._table.serdes.serialize_val(val), \
            col=None if col is None else self._table.serdes.serialize_col(col))
        return self
    
    def number_equals(self, val, col=None):
        '''Adds a condition to the query: the value of column *col* must
        be equal to *val*. Returns the same query object.
        '''
        self._tcquery.number_equals(self._table.serdes.serialize_val(val), \
            col=None if col is None else self._table.serdes.serialize_col(col))
        return self
    
    def greater_than(self, val, col=None):
        '''Adds a condition to the query: the value of column *col* must
        be greater than the number *val*. Returns the same query object.
        '''
        self._tcquery.greater_than(self._table.serdes.serialize_val(val), \
            col=None if col is None else self._table.serdes.serialize_col(col))
        return self
    
    def greater_or_equal(self, val, col=None):
        '''Adds a condition to the query: the value of column *col* must
        be greater than, or equal to, the number *val*.
        Returns the same query object.
        '''
        self._tcquery.greater_or_equal(self._table.serdes.serialize_val(val), \
            col=None if col is None else self._table.serdes.serialize_col(col))
        return self
    
    def less_than(self, val, col=None):
        '''Adds a condition to the query: the value of column *col* must
        be lower than the number *val*. Returns the same query object.
        '''
        self._tcquery.less_than(self._table.serdes.serialize_val(val), \
            col=None if col is None else self._table.serdes.serialize_col(col))
        return self
    
    def less_or_equal(self, val, col=None):
        '''Adds a condition to the query: the value of column *col* must
        be lower than, or equal to, the number *val*.
        Returns the same query object.
        '''
        self._tcquery.less_or_equal(self._table.serdes.serialize_val(val), \
            col=None if col is None else self._table.serdes.serialize_col(col))
        return self
    
    def between(self, low, high, col=None):
        '''Adds a condition to the query: the value of column *col* must
        be between the two numbers *low* and *high*.
        Returns the same query object.
        '''
        self._tcquery.between(self._table.serdes.serialize_val(low), \
            self._table.serdes.serialize_val(high), \
            col=None if col is None else self._table.serdes.serialize_col(col))
        return self
    
    def number_in(self, val, col=None):
        '''Adds a condition to the query: the value of column *col* must
        equal one of the numbers in *val*.
        *val* may be a list, or preferrably a string.
        Returns the same query object.
        '''
        serialize = self._table.serdes.serialize_val
        if isinstance(val, list):
            val = ' '.join([serialize(v) for v in val])
        self._tcquery.number_in(val, \
            col=None if col is None else self._table.serdes.serialize_col(col))
        return self



